在Unity中实现程序化音频
游戏的音频在很大的程度上可以影响游戏体验,所以在游戏开发中我们不得忽视音频开发的重要性。例如休闲小游戏《Rocket Plume》中,玩家利用火箭喷出的像素流来清理岩石,清理岩石的频繁音效一旦修饰不佳,容易让人感觉枯燥无味,从而带来不好的体验。
本文将由这款休闲小游戏的作者Joe Strout为大家分享他们在音频开发时遇到的问题和解决方案。下面是Joe Strout带来的分享内容。
音频开发问题
在开发《Rocket Plume》的过程中,音频方面主要遇到了两大难题。
第一个问题和火箭引擎发出的声音有关。在玩家游戏的大部分时间里引擎都是启动状态,无论怎样尝试着循环音频,玩家都能分辨出这是一段被反复播放的音频。这就会让游戏的声音听起来不太自然,分散玩家在游戏上的注意力。
第二个问题,也是最难搞定的问题,和游戏机制有关。在这款游戏的大部分时间里,玩家利用火箭喷出的像素流来清理岩石,且游戏中会非常频繁地重复这一过程,有时甚至会在一帧中摧毁多个像素的岩石。即便是用非常短的数字化声音,每摧毁一个像素就播放一次的话也会非常抓耳,让人感到枯燥乏味。我们也尝试了声音的各种修饰,使用随机声调和音量等,但效果仍然不理想。
程序化音频
对于《Rocket Plume》音频开发的两大难题,可行的解决方案就是程序化音频(Procedural Audio)。这是一种根据需求使用代码生成声音波形的方法。利用程序化音频能够使创造出永不重复的音频成为可能,同样也可以对游戏内发生的事情产生及时的反馈。
幸运的是,Unity支持直接在音频处理流中插入代码,这一鲜为人知但简单易用的方法使得程序化音频实现非常便捷。
Unity中的声音来源于Audio Source组件,并通过GameObject上的一个或多个Audio Filter组件修饰。这些滤波器组件按照组件列表中的顺序应用到声音中。Unity内置的滤波器组件包含高通及低通滤波器、回声(Echo)、畸变(Distortion)、混响(Reverb)和合唱(Chorus)效果。
如何创建程序化音频
创建程序化音频,只需新建一个MonoBehaviour子类,并实现OnAudioFilterRead方法。这样就实现了一个自定义音频滤波器。您可以根据喜好在音频处理链中加入滤波器。需要特别说明的是,要想凭空创造出声音,必须将您的音频滤波器放在音频处理栈中的音源组件之后、,所有其它音频滤波器之前。如果Audio Source组件不包含任何音频组件,该组件就会向滤波器链中直接输入零值,也就是说没有声音,而这就是依靠代码发声的原理。
多说无益,下面来看一些例子。
案例一
首先来解决第一个问题:引擎的噪音。和很多自然界中的声音一样,引擎噪音本质上就是被某些滤波器修饰后的白噪音——即完全随机的声波。因此第一个程序化音频脚本非常简单,它基本上就是用来生成白噪音的。
using UnityEngine;
public class EngineAudio : MonoBehaviour {
[Range(-1f, 1f)]
public float offset;
System.Random rand = new System.Random();
void OnAudioFilterRead(float[] data, int channels) {
for (int i = 0; i < data.Length; i++) {
data[i] = (float)(rand.NextDouble() * 2.0 - 1.0 + offset);
}
}
}
神奇的OnAudioFilterRead方法的接收参数是一个浮点数数组data和正在使用的频道数量channels。数组表示波形,以交叉格式存储:首先是各个频道的第一个样本,然后是各个频道的第二个样本,以此类推。该方法会被频繁调用(差不多是每20毫秒一次),同时还需要适当大小的数据缓存。每个数据采样的取值范围是-1到1,不在范围内的数值均忽略不计。
由于是在所有的频道生成白噪音,因此不需要关注这些细节,只需在每次采样时向缓冲区中填一个随机数就好了。这里对白噪音做了一点点偏移处理,也算是一种非常简单的音频滤波器,这样做可以避免采样的某些片段越界。
将该脚本添加到一个带有AudioSource组件的GameObject上,然后运行游戏,您会听到刺耳的“静电声”。适当调整偏移量,可以让声音缓和一点。但我们并不想让玩家听到这样的声音,还需要添加更多的音频滤波器。我们还使用了音频低通滤波器,将截止频率(cutoff frequency)设置为800左右,Q参数设置为1。接着使用音频失真滤波器,并将失真水平(distortion level)设置为0.5。这样就得到了一种平滑丰富且听起来非常像引擎的声音。因为这种声音是即时生成的,所以它不会让人感到重复单调。
注意,以上代码是简单的范例,实际上《Rocket Plume》游戏中不是直接使用。因为直接打开或关闭(例如启用或禁用这个GameObject)这个引擎噪音带来的体验不是太好,对比引擎关闭后的寂静,引擎再次开启后简直震耳欲聋,尤其是在玩家关掉了背景音乐的情况下。
最后我们决定不完全关闭引擎的噪音,而是在引擎“空闲”时使用一种音量很低的背景音。测试过一些音频滤波器后,我们发现减少低通滤波频率可以达到这种效果,然后将这种机制与引擎的开关挂钩。我们对此有过一些争论,即这些脚本是否和白噪音生成器属于相同类型的程序,不过最后我们还是决定把所有引擎相关的代码保存到一起。
最终的脚本如下所示:
using UnityEngine;
public class EngineAudio : MonoBehaviour {
[Range(-1f, 1f)]
public float offset;
public float cutoffOn = 800;
public float cutoffOff = 100;
public bool engineOn;
System.Random rand = new System.Random();
AudioLowPassFilter lowPassFilter;
void Awake() {
lowPassFilter = GetComponent<AudioLowPassFilter>();
Update();
}
void OnAudioFilterRead(float[] data, int channels) {
for (int i = 0; i < data.Length; i++) {
data[i] = (float)(rand.NextDouble() * 2.0 - 1.0 + offset);
}
}
void Update() {
lowPassFilter.cutoffFrequency = engineOn ? cutoffOn : cutoffOff;
}
}
案例二
接下来我们来看一下第二个问题,这个问题是关于岩石破裂音的,当火箭尾流消解掉岩石时就会发出这样的声音。这种声音不仅仅是用来取悦玩家耳朵的,它也是一种重要的反馈,能告知玩家发生了什么事情,例如了解何时打开了通路可以继续前进了。再次强调,白噪音是声音系统的核心。这次只想在像素被炸开时发出一点点轻微的白噪音。我们称这些爆炸声为“click”,这些声音听起来是互相独立的,没有经过任何过滤。这里的音频脚本带有一个点击计数器,当像素被摧毁时这个计数器就会增长(增长操作由其他代码负责),然后滤波器代码负责消耗掉这些计数并生成“click”声,这也可以保证生成的“click”声的数量总是与消除像素数量一致。
脚本代码如下:
using UnityEngine;
public class RockChewAudio : MonoBehaviour {
public static int clicks = 0;
System.Random rand = new System.Random();
void OnAudioFilterRead(float[] data, int channels) {
bool inClick = false; // 我们是否生成了 click 或 silence
int samplesLeft = 0; // 我们需要历经多少 click 或 silence
for (int i = 0; i < data.Length; i += channels) {
if (samplesLeft < 1) {
// If out of clicks, then just generate silence for the rest of the time.
if (clicks < 1) {
inClick = false;
samplesLeft = data.Length / channels;
} else if (inClick) {
// Generate a small random silence.
inClick = false;
samplesLeft = rand.Next(1,10);
} else {
// Generate a click.
inClick = true;
samplesLeft = rand.Next(2,5);
clicks--;
}
}
for (int j=0; j<channels; j++) {
data[i+j] = inClick ? (float)(rand.NextDouble() * 2.0 - 1.0) : 0;
}
samplesLeft--;
}
clicks = 0;
}
}
这里的for主循环稍稍有些复杂, 因为牵涉到了频道。不过基本思路还是一样的:迭代所有的采样样本,生成“click”音(白噪音),或不生成声音。当前阶段的“click”片段或静音片段的所有样本生成完毕后,再继续生成下一段。所有需要的“click”生成完毕后,让缓冲区中剩余的部分保持静音即可。
和引擎噪音一样,这里也使用了低通和失真音频滤波器。低通截止值要高一些(3000左右)。
最终的结果是,岩石爆炸时听起来更像是基本粒子互相碰撞融合的声音。不过再怎么去描述也不如亲耳去听一下,查看下方游戏视频,您可以听到所有的最终效果。
https://v.qq.com/txp/iframe/player.html?vid=n03839470xt&width=500&height=375&auto=0
总结
希望这篇文章能够给大家一个清晰的认识,即在Unity中制作出这样的程序化音频是非常容易的。放开自己的想象力,摆脱掉那些单调乏味的声音效果,考虑为您的游戏加入一些这样的程序化音效吧。如果您想了解更多Unity中音频有关的内容,请点击【阅读原文】访问Unity官方中文社区(forum.china.unity3d.com)!
更多Unity相关技术文章
Unity崩溃报告自动处理工具:Crash Analyzer
Unite 2017 Shanghai
Unite 2017 Shanghai将于5月11 - 13日在上海国际会议中心举行。5折个人通票开售!赞助商招募已开启,愿您与我们一同打造一场Unity开发者盛会!Made with Unity展区作品征集等更多信息请访问Unite 2017 Shanghai官方网站(unite2017.csdn.net)!
点击“阅读原文”访问Unity官方中文社区!